#!/usr/bin/env python

import os
import sys
import argparse
import logging

from k4FWCore.utils import load_file

# these default properties are filtered as otherwise they will clutter the argument list
FILTER_GAUDI_PROPS = [
    "ContextService",
    "Cardinality",
    "Context",
    "CounterList",
    "EfficiencyRowFormat",
    "Enable",
    "ErrorCount",
    "ErrorMax",
    "ErrorsPrint",
    "ExtraInputs",
    "ExtraOutputs",
    "FilterCircularDependencies",
    "StatEntityList",
    "IsIOBound",
    "MonitorService",
    "NeededResources",
    "PropertiesPrint",
    "RegisterForContextService",
    "RegularRowFormat",
    "RequireObjects",
    "RootInTES",
    "StatPrint",
    "StatTableHeader",
    "Timeline",
    "TypePrint",
    "UseEfficiencyRowFormat",
    "EnableFaultHandler",
    "InhibitPathes",
    "EnableAccessHandler",
    "ForceLeaves",
    "RootName",
    "DataAccessName",
    "DataFaultName",
    "RootCLID",
    "PrintEmptyCounters",
    "Blocking",
    "VetoObjects",
    "CheckToolDeps",
    "AutoRetrieveTools",
]

# ---------------------------------------------------------------------

seen_files = set()
option_db = {}


def add_arguments(parser, app_mgr):
    # length increases when properties of an algorithm with tools are inspected
    # see https://github.com/key4hep/k4FWCore/pull/138
    # can contain the same value multiple times
    # see https://github.com/key4hep/k4FWCore/issues/141
    for conf in frozenset(app_mgr.allConfigurables.values()):
        # skip public tools and the applicationmgr itself
        if "ToolSvc" in conf.name() or "ApplicationMgr" in conf.name():
            continue
        props = (
            conf.getPropertiesWithDescription()
        )  # dict propertyname: (propertyvalue, propertydescription)
        for prop in props:
            # only add arguments for relevant properties
            if (
                prop in FILTER_GAUDI_PROPS
                or "Audit" in prop
                or hasattr(props[prop][0], "__slots__")
            ):
                continue
            propvalue = props[prop][0]

            # if it is set to "no value" it hasn't been touched in the options file
            if propvalue == conf.propertyNoValue:
                propvalue = conf.getDefaultProperty(prop)  # thus get the default value
            proptype = type(props[prop][0])
            # if the property is a list of something, we need to set argparse nargs to '+'
            propnargs = "?"
            if proptype == list:
                # tricky edgecase: if the default is an empty list there is no way to get the type
                if len(propvalue) == 0:
                    # just skip for now
                    # print("Warning: argparse cannot deduce type for property %s of %s. Needs to be set in options file." % (prop, conf.name()))
                    continue
                else:
                    # deduce type from first item of the list
                    proptype = type(propvalue[0])
                propnargs = "+"

            # add the argument twice, once as "--PodioOutput.filename"
            # and once as "--filename.PodioOutput"
            propName = conf.name() + "." + prop
            propNameReversed = prop + "." + conf.name()
            option_db[propName] = (conf, propName)
            parser.add_argument(
                f"--{propName}",
                f"--{propNameReversed}",
                type=proptype,
                help=props[prop][1],
                nargs=propnargs,
                default=propvalue,
            )


class LoggingHandler(logging.StreamHandler):
    def handleError(self, record):
        # Re-raise SIGPIPE generated exception
        excp = sys.exc_info()[1]  # from Python 3.11 can be replaced with sys.exception()
        if isinstance(excp, BrokenPipeError):
            raise excp

        super().handleError(record)


def main():
    # ensure that we (and the subprocesses) use the C standard localization
    os.environ["LC_ALL"] = "C"

    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    # formatter = logging.Formatter('[%(asctime)s %(levelname)s] %(message)s', datefmt='%Y-%b-%d %H:%M:%S')
    formatter = logging.Formatter("[k4run] %(message)s")
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(formatter)
    logger.handlers = [handler]

    from k4FWCore.parseArgs import parser

    parser.add_argument(
        "config_files",
        type=open,
        action="store",
        nargs="*",
        help="Gaudi config (python) files describing the job",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Do not actually run the job, just parse the config files",
    )
    parser.add_argument("-v", "--verbose", action="store_true", help="Run job with verbose output")
    parser.add_argument("-n", "--num-events", type=int, help="Number of events to run")
    parser.add_argument(
        "-l",
        "--list",
        action="store_true",
        help="Print all the configurable components available in the framework and exit",
    )
    parser.add_argument("--gdb", action="store_true", help="Attach gdb debugger")

    # Once to parse the files and another time below to have the new parameters
    # in the namespace since parsing of the file happens when the namespace
    # has already been filled
    opts = parser.parse_known_args()

    if opts[0].list:
        from Gaudi import Configuration

        cfgDb = Configuration.cfgDb
        logger.info("Available components:\n%s", (21 * "="))
        for item in sorted(cfgDb):
            if True:  # another option could filter Gaudi components here
                try:
                    path_to_component = __import__(cfgDb[item]["module"]).__file__
                except ImportError:
                    path_to_component = "NotFound"
                print(
                    "  %s (from %s), \n\t\t path: %s\n"
                    % (item, cfgDb[item]["lib"], path_to_component)
                )
            else:
                if "Gaudi" not in cfgDb[item]["lib"]:
                    print("  %s (from %s)" % (item, cfgDb[item]["lib"]))
        sys.exit()

    for file in opts[0].config_files:
        load_file(file)

    # ApplicationMgr is a singleton
    from Configurables import ApplicationMgr

    add_arguments(parser, ApplicationMgr())

    # add help manually here, if it is added earlier the parser exits after the first parse_arguments call
    parser.add_argument(
        "-h",
        "--help",
        action="help",
        default=argparse.SUPPRESS,
        help="show this help message and exit",
    )

    opts = parser.parse_args()

    if len(opts.config_files) == 0:
        print(
            "Error: missing gaudi options file.\n"
            "Usage: k4run <options_file.py>, use --help to get a complete list of arguments"
        )
        sys.exit(1)

    # print a doc line showing the configured algorithms
    logger.info(" ".join(f"--> {alg.name()}" for alg in ApplicationMgr().TopAlg))

    opts_dict = vars(opts)
    for optionName, propTuple in option_db.items():
        logger.info("Option name: %s %s %s", propTuple[1], optionName, opts_dict[optionName])
        propTuple[0].setProp(propTuple[1].rsplit(".", 1)[1], opts_dict[optionName])

    if opts.verbose:
        from Gaudi.Configuration import VERBOSE

        ApplicationMgr().OutputLevel = VERBOSE
    if opts.num_events is not None:
        ApplicationMgr().EvtMax = opts.num_events
    # Allow graceful exit with Ctrl + C
    ApplicationMgr().StopOnSignal = True

    from Gaudi.Main import gaudimain

    c = gaudimain()
    if not opts.dry_run:
        # Do the real processing
        retcode = c.run(opts.gdb)
        # User requested stop returns non-zero exit code see: https://github.com/key4hep/k4FWCore/issues/125
        if retcode == 4:
            retcode = 0
        sys.exit(retcode)


if __name__ == "__main__":
    try:
        main()
    except BrokenPipeError:
        # Handle SIGPIPE generated exception
        devnull = os.open(os.devnull, os.O_WRONLY)
        os.dup2(devnull, sys.stdout.fileno())
        os.dup2(devnull, sys.stderr.fileno())
        sys.exit(1)
